﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using Ignitron.EEngine.Adapters.DX.System;
using Ignitron.EEngine.Lighting;
using Ignitron.EEngine.Adapters.DX.Helper;
using SharpDX;
using SharpDX.D3DCompiler;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using Buffer = SharpDX.Direct3D11.Buffer;
using Device = SharpDX.Direct3D11.Device;

namespace Ignitron.EEngine.Adapters.DX.Graphics
{
    /// <summary>
    /// Shader used for rendering lighted models 
    /// </summary>
    internal class LightingShader: ShaderBase
    {
        /// <summary>
        /// Lighting shader structures
        /// </summary>
        #region Structures

        #region Pixel Shader Structures
        
        // Structure representing mesh material record
        [StructLayout(LayoutKind.Sequential)]
        internal struct Material
        {
            public Vector4 Ambient;
            public Vector4 Diffuse;
            public Vector4 Specular;
            public float Shininess;
        }

        // Structure for representing a light
        [StructLayout(LayoutKind.Sequential)]
        internal struct Light
        {
            public Vector4 Ambient;
            public Vector4 Diffuse;
            public Vector4 Specular;
            public Vector4 Position;
            public float ConstantAttenuation;
            public float LinearAttenuation;
            public float QuadraticAttenuation;
            public float IsActive;
        }

        // The main light buffer
        [StructLayout(LayoutKind.Sequential)]
        internal struct LightBuffer
        {
            public Material MeshMaterial;
            public Vector3 GlobalAmbient;

            public Light Light0;
            public Light Light1;
            public Light Light2;
            public Light Light3;
            public Light Light4;
            public Light Light5;
            public Light Light6;
            public Light Light7;

        }

        #endregion

        #region Vertex Shader Structures
        
        /// <summary>
        /// The Vertex format 
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        internal struct Vertex
        {
            public static int AppendAlignedElement1 = 16;
            public static int AppendAlignedElement2 = 24;

            public Vector4 position;
            public Vector2 texture;
            public Vector3 normal;
        }

        /// <summary>
        /// The camera position 
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        internal struct CameraBuffer
        {
            public Vector3 CameraPosition;
            public float AlphaOpacity;
        }

        #endregion


        #endregion

        /// <summary>
        /// Shader Buffers
        /// </summary>
        #region Variables / Properties

        Buffer ConstantLightBuffer { get; set; }
        Buffer ConstantCameraBuffer { get; set; }

        #endregion

        #region Initialization and Shutdown

        /// <summary>
        /// Creates the structure of input elements for the shader
        /// </summary>
        protected override InputElement[] CreateInputElements()
        {
            // Now setup the layout of the data that goes into the shader.
            // This setup needs to match the VertexType structure in the Model and in the shader.
            var inputElements = new InputElement[]
				{
					new InputElement()
					{
						SemanticName = "POSITION",
						SemanticIndex = 0,
						Format = Format.R32G32B32A32_Float,
						Slot = 0,
						AlignedByteOffset = 0,
						Classification = InputClassification.PerVertexData,
						InstanceDataStepRate = 0
					},
					new InputElement()
					{
						SemanticName = "TEXCOORD",
						SemanticIndex = 0,
						Format = Format.R32G32_Float,
						Slot = 0,
						AlignedByteOffset = Vertex.AppendAlignedElement1,
						Classification = InputClassification.PerVertexData,
						InstanceDataStepRate = 0
					},
					new InputElement()
					{
						SemanticName = "NORMAL",
						SemanticIndex = 0,
						Format = Format.R32G32B32_Float,
						Slot = 0,
						AlignedByteOffset = Vertex.AppendAlignedElement2,
						Classification = InputClassification.PerVertexData,
						InstanceDataStepRate = 0
					}
				};

            return inputElements;
        }

        /// <summary>
        /// Initializes the shader
        /// </summary>
        protected override bool InitializeShader(Device device, IntPtr windowHandler, string vsFileName, string psFileName)
        {
            try
            {
                CompileShaders(device, windowHandler, vsFileName, psFileName, "LightVertexShader", "LightPixelShader");

                // Create a texture sampler state description.
                var samplerDesc = new SamplerStateDescription()
                {
                    Filter = Filter.MinMagMipLinear,
                    AddressU = TextureAddressMode.Wrap,
                    AddressV = TextureAddressMode.Wrap,
                    AddressW = TextureAddressMode.Wrap,
                    MipLodBias = 0,
                    MaximumAnisotropy = 1,
                    ComparisonFunction = Comparison.Always,
                    BorderColor = new Color4(0, 0, 0, 0),
                    MinimumLod = 0,
                    MaximumLod = float.MaxValue
                };

                // Create the texture sampler state.
                SampleState = new SamplerState(device, samplerDesc);

                // Setup the description of the dynamic matrix constant buffer that is in the vertex shader.
                var matrixBufferDesc = new BufferDescription()
                {
                    Usage = ResourceUsage.Dynamic,
                    SizeInBytes = Utilities.SizeOf<MatrixBuffer>(),
                    BindFlags = BindFlags.ConstantBuffer,
                    CpuAccessFlags = CpuAccessFlags.Write,
                    OptionFlags = ResourceOptionFlags.None,
                    StructureByteStride = 0
                };

                // Create the constant buffer pointer so we can access the vertex shader constant buffer from within this class.
                ConstantMatrixBuffer = new Buffer(device, matrixBufferDesc);

                // Setup the description of the camera dynamic constant buffer that is in the vertex shader.
                var cameraBufferDesc = new BufferDescription()
                {
                    Usage = ResourceUsage.Dynamic,
                    SizeInBytes = Utilities.SizeOf<CameraBuffer>(),
                    BindFlags = BindFlags.ConstantBuffer,
                    CpuAccessFlags = CpuAccessFlags.Write,
                    OptionFlags = ResourceOptionFlags.None,
                    StructureByteStride = 0
                };

                // Create the constant buffer pointer so we can access the vertex shader constant buffer from within this class.
                ConstantCameraBuffer = new Buffer(device, cameraBufferDesc);

                // Setup the description of the light dynamic constant bufffer that is in the pixel shader.
                // Note that ByteWidth alwalys needs to be a multiple of the 16 if using D3D11_BIND_CONSTANT_BUFFER or CreateBuffer will fail.
                var lightBufferDesc = new BufferDescription()
                {
                    Usage = ResourceUsage.Dynamic,
                    SizeInBytes = Utilities.SizeOf<LightBuffer>(),
                    BindFlags = BindFlags.ConstantBuffer,
                    CpuAccessFlags = CpuAccessFlags.Write,
                    OptionFlags = ResourceOptionFlags.None,
                    StructureByteStride = 0
                };

                // Create the constant buffer pointer so we can access the vertex shader constant buffer from within this class.
                ConstantLightBuffer = new Buffer(device, lightBufferDesc);

                return true;
            }
            catch (Exception)
            {
                //MessageBox.Show("Error initializing shader. Error is " + ex.Message);
                return false;
            };
        }

        /// <summary>
        /// Relases the shader
        /// </summary>
        protected override void ShuddownShader()
        {
            // Release the light constant buffer.
            if (ConstantLightBuffer != null)
            {
                ConstantLightBuffer.Dispose();
                ConstantLightBuffer = null;
            }

            // Release the matrix constant buffer.
            if (ConstantCameraBuffer != null)
            {
                ConstantCameraBuffer.Dispose();
                ConstantCameraBuffer = null;
            }

            base.ShuddownShader();
        }

        #endregion


        #region Public API

        public bool Render(int indexCount, ShaderResourceView texture, Vector3 cameraPosition, float alpha, LightingManager lightingManager)
        {
            // Set the shader parameters that it will use for rendering.
            if (!SetShaderParameters(texture, cameraPosition, alpha, lightingManager))
                return false;

            // Now render the prepared buffers with the shader.
            RenderIndexed(mDevice.ImmediateContext, indexCount);

            return true;
        }

        #endregion

        private bool SetShaderParameters(ShaderResourceView texture, Vector3 cameraPosition, float alpha, LightingManager lightingManager)
        {
            try
            {

                DataStream mappedResource;

                #region Constant Matrix Buffer

                // copy values
                var deviceContext = mDevice.ImmediateContext;

                var worldMatrix = mWorldMatrix;
                var viewMatrix = mViewMatrix;
                var projectionMatrix = mProjectionMatrix;

                // Transpose the matrices to prepare them for shader.
                worldMatrix.Transpose();
                viewMatrix.Transpose();
                projectionMatrix.Transpose();

                // Lock the matrix constant buffer so it can be written to.
                deviceContext.MapSubresource(ConstantMatrixBuffer, MapMode.WriteDiscard, SharpDX.Direct3D11.MapFlags.None, out mappedResource);

                // Copy the matrices into the constant buffer.
                var matrixBuffer = new MatrixBuffer()
                {
                    world = worldMatrix,
                    view = viewMatrix,
                    projection = projectionMatrix
                };

                mappedResource.Write(matrixBuffer);

                // Unlock the constant buffer.
                deviceContext.UnmapSubresource(ConstantMatrixBuffer, 0);

                // Set the position of the constant buffer in the vertex shader.
                var bufferNumber = 0;

                // Finally set the constant buffer in the vertex shader with the updated values.
                deviceContext.VertexShader.SetConstantBuffer(bufferNumber, ConstantMatrixBuffer);

                // Set shader resource in the pixel shader.
                deviceContext.PixelShader.SetShaderResource(0, texture);
                #endregion

                // TO-DO: Camera buffer
                #region Constant Camera Buffer
                // Lock the camera constant buffer so it can be written to.
                deviceContext.MapSubresource(ConstantCameraBuffer, MapMode.WriteDiscard, SharpDX.Direct3D11.MapFlags.None, out mappedResource);

                // Copy the lighting variables into the constant buffer.
                var cameraBuffer = new CameraBuffer()
                {
                    CameraPosition = cameraPosition,
                    AlphaOpacity = alpha
                };

                mappedResource.Write(cameraBuffer);

                // Unlock the constant buffer.
                deviceContext.UnmapSubresource(ConstantCameraBuffer, 0);

                // Set the position of the light constant buffer in the pixel shader.
                bufferNumber = 1;

                // Now set the camera constant buffer in the vertex shader with the updated values.
                deviceContext.VertexShader.SetConstantBuffer(bufferNumber, ConstantCameraBuffer);
                #endregion

                #region Constant Light Buffer
                // Lock the light constant buffer so it can be written to.
                deviceContext.MapSubresource(ConstantLightBuffer, MapMode.WriteDiscard, SharpDX.Direct3D11.MapFlags.None, out mappedResource);

                // Copy the lighting variables into the constant buffer.
                var lightBuffer = new LightBuffer();

                // set material
                EMaterial eMat = lightingManager.GetMaterial();
                lightBuffer.MeshMaterial.Ambient = eMat.Ambient.ToDX();
                lightBuffer.MeshMaterial.Diffuse = eMat.Diffuse.ToDX();
                lightBuffer.MeshMaterial.Specular = eMat.Specular.ToDX();
                lightBuffer.MeshMaterial.Shininess = eMat.Shininess;

                // set the lights
                lightBuffer = UpdateLights(lightBuffer, lightingManager);
                lightBuffer.GlobalAmbient = lightingManager.GetGlobalAmbientLight();             

                mappedResource.Write(lightBuffer);

                // Unlock the constant buffer.
                deviceContext.UnmapSubresource(ConstantLightBuffer, 0);

                // Set the position of the light constant buffer in the pixel shader.
                bufferNumber = 0;

                // Finally set the light constant buffer in the pixel shader with the updated values.
                deviceContext.PixelShader.SetConstantBuffer(bufferNumber, ConstantLightBuffer);
                #endregion

                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        /// <summary>
        /// Updates the lights' settings
        /// </summary>
        private LightBuffer UpdateLights(LightBuffer lightBuffer, LightingManager lightingManager)
        {
            // Light 0
            int index = 0;
            if (lightingManager.LightCount > index)
            {
                var light = lightingManager.GetLight(index);
                lightBuffer.Light0.IsActive = light.IsTurnedOn ? 1f : -1f;
                lightBuffer.Light0.Ambient = light.Ambient.ToDX();
                lightBuffer.Light0.Diffuse = light.Diffuse.ToDX();
                lightBuffer.Light0.Specular = light.Specular.ToDX();
                lightBuffer.Light0.ConstantAttenuation = light.ConstantAttenuation;
                lightBuffer.Light0.LinearAttenuation = light.LinearAttenuation;
                lightBuffer.Light0.QuadraticAttenuation = light.QuadraticAttenuation;
                lightBuffer.Light0.Position = lightingManager.GetLightPosition(index);
            }
            else
            {
                lightBuffer.Light0.IsActive = -1f;
            }

            // Light 1
            index = 1;
            if (lightingManager.LightCount > index)
            {
                var light = lightingManager.GetLight(index);
                lightBuffer.Light1.IsActive = light.IsTurnedOn ? 1f : -1f;
                lightBuffer.Light1.Ambient = light.Ambient.ToDX();
                lightBuffer.Light1.Diffuse = light.Diffuse.ToDX();
                lightBuffer.Light1.Specular = light.Specular.ToDX();
                lightBuffer.Light1.ConstantAttenuation = light.ConstantAttenuation;
                lightBuffer.Light1.LinearAttenuation = light.LinearAttenuation;
                lightBuffer.Light1.QuadraticAttenuation = light.QuadraticAttenuation;
                lightBuffer.Light1.Position = lightingManager.GetLightPosition(index);
            }
            else
            {
                lightBuffer.Light1.IsActive = -1f;
            }

            // Light 2
            index = 2;
            if (lightingManager.LightCount > index)
            {
                var light = lightingManager.GetLight(index);
                lightBuffer.Light2.IsActive = light.IsTurnedOn ? 1f : -1f;
                lightBuffer.Light2.Ambient = light.Ambient.ToDX();
                lightBuffer.Light2.Diffuse = light.Diffuse.ToDX();
                lightBuffer.Light2.Specular = light.Specular.ToDX();
                lightBuffer.Light2.ConstantAttenuation = light.ConstantAttenuation;
                lightBuffer.Light2.LinearAttenuation = light.LinearAttenuation;
                lightBuffer.Light2.QuadraticAttenuation = light.QuadraticAttenuation;
                lightBuffer.Light2.Position = lightingManager.GetLightPosition(index);
            }
            else
            {
                lightBuffer.Light2.IsActive = -1f;
            }

            // Light 3
            index = 3;
            if (lightingManager.LightCount > index)
            {
                var light = lightingManager.GetLight(index);
                lightBuffer.Light3.IsActive = light.IsTurnedOn ? 1f : -1f;
                lightBuffer.Light3.Ambient = light.Ambient.ToDX();
                lightBuffer.Light3.Diffuse = light.Diffuse.ToDX();
                lightBuffer.Light3.Specular = light.Specular.ToDX();
                lightBuffer.Light3.ConstantAttenuation = light.ConstantAttenuation;
                lightBuffer.Light3.LinearAttenuation = light.LinearAttenuation;
                lightBuffer.Light3.QuadraticAttenuation = light.QuadraticAttenuation;
                lightBuffer.Light3.Position = lightingManager.GetLightPosition(index);
            }
            else
            {
                lightBuffer.Light3.IsActive = -1f;
            }

            // Light 4
            index = 4;
            if (lightingManager.LightCount > index)
            {
                var light = lightingManager.GetLight(index);
                lightBuffer.Light4.IsActive = light.IsTurnedOn ? 1f : -1f;
                lightBuffer.Light4.Ambient = light.Ambient.ToDX();
                lightBuffer.Light4.Diffuse = light.Diffuse.ToDX();
                lightBuffer.Light4.Specular = light.Specular.ToDX();
                lightBuffer.Light4.ConstantAttenuation = light.ConstantAttenuation;
                lightBuffer.Light4.LinearAttenuation = light.LinearAttenuation;
                lightBuffer.Light4.QuadraticAttenuation = light.QuadraticAttenuation;
                lightBuffer.Light4.Position = lightingManager.GetLightPosition(index);
            }
            else
            {
                lightBuffer.Light4.IsActive = -1f;
            }

            // Light 5
            index = 5;
            if (lightingManager.LightCount > index)
            {
                var light = lightingManager.GetLight(index);
                lightBuffer.Light5.IsActive = light.IsTurnedOn ? 1f : -1f;
                lightBuffer.Light5.Ambient = light.Ambient.ToDX();
                lightBuffer.Light5.Diffuse = light.Diffuse.ToDX();
                lightBuffer.Light5.Specular = light.Specular.ToDX();
                lightBuffer.Light5.ConstantAttenuation = light.ConstantAttenuation;
                lightBuffer.Light5.LinearAttenuation = light.LinearAttenuation;
                lightBuffer.Light5.QuadraticAttenuation = light.QuadraticAttenuation;
                lightBuffer.Light5.Position = lightingManager.GetLightPosition(index);
            }
            else
            {
                lightBuffer.Light5.IsActive = -1f;
            }

            // Light 6
            index = 6;
            if (lightingManager.LightCount > index)
            {
                var light = lightingManager.GetLight(index);
                lightBuffer.Light6.IsActive = light.IsTurnedOn ? 1f : -1f;
                lightBuffer.Light6.Ambient = light.Ambient.ToDX();
                lightBuffer.Light6.Diffuse = light.Diffuse.ToDX();
                lightBuffer.Light6.Specular = light.Specular.ToDX();
                lightBuffer.Light6.ConstantAttenuation = light.ConstantAttenuation;
                lightBuffer.Light6.LinearAttenuation = light.LinearAttenuation;
                lightBuffer.Light6.QuadraticAttenuation = light.QuadraticAttenuation;
                lightBuffer.Light6.Position = lightingManager.GetLightPosition(index);
            }
            else
            {
                lightBuffer.Light6.IsActive = -1f;
            }

            // Light 7
            index = 7;
            if (lightingManager.LightCount > index)
            {
                var light = lightingManager.GetLight(index);
                lightBuffer.Light7.IsActive = light.IsTurnedOn ? 1f : -1f;
                lightBuffer.Light7.Ambient = light.Ambient.ToDX();
                lightBuffer.Light7.Diffuse = light.Diffuse.ToDX();
                lightBuffer.Light7.Specular = light.Specular.ToDX();
                lightBuffer.Light7.ConstantAttenuation = light.ConstantAttenuation;
                lightBuffer.Light7.LinearAttenuation = light.LinearAttenuation;
                lightBuffer.Light7.QuadraticAttenuation = light.QuadraticAttenuation;
                lightBuffer.Light7.Position = lightingManager.GetLightPosition(index);
            }
            else
            {
                lightBuffer.Light7.IsActive = -1f;
            }

            return lightBuffer;
        }
    }
}
